# -*- coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

__all__ = ['Application', 'PluginManager',
           'APPLICATION', 'find_app_plugins', 'find_plugins',
           'display_application_infos']

import os
import codecs
import re
import logging
import warnings

from envisage.api import Application as EnvisageApplication
from envisage.api import ServiceRegistry as EnvisageServiceRegistry
from envisage.api import Plugin
from traits import interface_checker

#import inspect
from _pylot.minimal.utils import check_implements
from _pylot.minimal.exception import CustomException

from pyLot.minimal import (InheritableSingleton, UserConfigDir, ENCODING, Path,
                           log_exception, get_data, DEFAULT_PROCESS_HANDLER)

LOGGER = logging.getLogger('pyLot.application')

class ErrServiceNotFound(CustomException):
  u"""
  ErrServiceNotFound('interface')
  """
  title = u'Error: no service found'
  message = u'No service found for interface %(interface_path)s'
  desc = "Please check paths and %(plugin_path)s"

  def _kargs(self):
    return dict(
      interface=self._args[0],
      interface_name=unicode(self._args[0].__name__),
      interface_path=u'%s.%s' % (self._args[0].__module__, self._args[0].__name__),
      plugin_path=Path(APPLICATION.home, u'plugin_list.txt')
      )

class ServiceRegistery(EnvisageServiceRegistry):
  def _is_service_factory(self, protocol, obj):
    """ Is the object a factory for services supporting the protocol? """
    try :
      check_implements(u'service', obj, protocol)
    except interface_checker.InterfaceError :
      is_service_factory = True
    else :
      is_service_factory = False
    return is_service_factory


class ErrConfDirNotValid(CustomException):
  title = u'Error: specified configuration directory %(value)s is not valid'
  message = title
  desc = title

from pyLot.recording import Recorder, set_recorder

from .core_plugin import CorePlugin

interfaces = [Plugin]

def find_plugins(plugin_dict, pattern=r'.*\.py'):
  u"""
  Find all plugins matching pattern.
  To find plugins, this methods scan all files defined by plugin_dict.
  plugin_dict keys are directory paths containing plugins.
  plugin_dict values is a list. If list is empty, all files matching patterns 
  are scanned, else, only path in list.
  
  example :
    from pyLot.core import Path
    find_plugins({Path(u'/.../plugins'):[])
  
  :param plugin_dict: dict plugin_path: list of filepath
  """
  plugin_dirs = plugin_dict.keys()
  regexpr = re.compile(pattern)
  plugins = []
  modpaths = []
  for module_dir in plugin_dirs :
    if not module_dir.isdir() :
      continue
    elif plugin_dict[module_dir] :
      for class_path in plugin_dict[module_dir] :

        module_name = class_path.split(u'.')[0:-1]
        obj_name = class_path.split(u'.')[-1]

        module_name.insert(0, module_dir.name)
        modpath = u'.'.join(module_name)

        try :
          mod = __import__(modpath, fromlist=[modpath])
        except ImportError, error:
          LOGGER.info(log_exception(error))
          continue

        try :
          obj = getattr(mod, obj_name)
        except AttributeError, error:
          LOGGER.info(log_exception(error))
          continue
        else :
          if issubclass(obj, Plugin) and obj not in interfaces :
            plugins.append(obj)
    else :
      for pyfile in module_dir.walkfiles_re(regexpr) :
        modpath = module_dir.parent.relpathto(pyfile)
        for ext in (u'.pyc', u'.pyo', u'.pyd', u'.py'):
          if modpath.endswith(ext) :
            modpath = modpath.replaceext(u'', ext)
            break
        modpath = '.'.join(modpath.splitall()[1:])
        modpath = modpath.replace(os.sep, u'.')
        if modpath not in modpaths :
          modpaths.append(modpath)
          if APPLICATION.DEBUG_PLUGINS :
            mod = __import__(modpath, fromlist=[modpath])
          else :
            try :
              mod = __import__(modpath, fromlist=[modpath])
            except Exception, error:
              LOGGER.info(log_exception(error))
              continue
          for obj_name in dir(mod):
            obj = getattr(mod, obj_name)
            try :
              if issubclass(obj, Plugin) and obj not in interfaces :
                plugins.append(obj)
            except TypeError :
              pass
        else :
          continue
  return plugins

def find_app_plugins(confdir, pattern=r'.*plugindef.*\.py',
                     default_list=None):
  if default_list is None :
    default_list = []
  plugin_lst_path = Path(confdir, u'plugin_list.txt')
  if not plugin_lst_path.exists() :
    plugin_lst = codecs.open(plugin_lst_path, u'w', u'utf-8')
    for lst in default_list :
      plugin_lst.write(u'%s\n' % lst)
    plugin_lst.close()

  plugin_dict = {}
  plugin_lst = codecs.open(plugin_lst_path, u'r', u'utf-8')
  pack = None
  for line in plugin_lst :
    line = line.strip()
    if line.startswith(u'#') or not line :
      continue

    if line.startswith(u'-') :
      plugin_class_path = line[1:].lstrip()
      if pack is None :
        LOGGER.info('Missing pack name for plugin "%s"' % plugin_class_path)
      else :
        plugin_dict[pack].append(plugin_class_path)
    elif line :
      plugin_path = Path(line)
      if plugin_path.isabs() :
        pack = plugin_path
      else :
        try :
          plugin_path = get_data(plugin_path, u'.')
        except ImportError :
          LOGGER.info('plugin module name "%s" is not valid' % plugin_path)
          continue
        else :
          pack = plugin_path

      if pack :
        if pack not in plugin_dict :
          plugin_dict[pack] = []

  return find_plugins(plugin_dict, pattern)

def display_plugin_infos(plugin):
  print '-' * 80
  if isinstance(plugin, Plugin) :
    print '  - Plugin.id: %s' % plugin.id
    print '  - Nom: %s' % plugin.name
  else :
    print '  - Objet.id: %s' % plugin.id
  print
  print '  repr: %r' % plugin
  print '  type: %s' % type(plugin)
  print '  home: %s' % plugin.home
  print
  print "  Points d'extension :"
  ext_pts = plugin.get_extension_points()
  ext_pts.sort()
  tot = 0
  for ext_pt in ext_pts :
    n = len(plugin.get_extensions(ext_pt.id))
    tot += n
    print '    *** \033[1;31m%s\033[37m (\033[1;36m%d\033[37m extensions)' % (ext_pt.id, n)
  print '      \033[1;36m%d\033[37m extensions chargées' % tot

def display_application_infos(application):
  from envisage.api import ServiceOffer
  if not isinstance(application, EnvisageApplication) :
    return
  display_plugin_infos(application)

  print
  print
  ext_pts = application.get_extension_points()
  ext_pts.sort()
  for ext_pt in ext_pts :
    print '-' * 80
    print 'Point d\'extension : \033[1;31m%s\033[37m \n' % ext_pt.id
    for plugin in application :
      exts = plugin.get_extensions(ext_pt.id)
      if exts :
        try :
          bases = exts[0].__implements__.getBases()
        except AttributeError :
          interface = '??'
        else :
          interface = ', '.join([base.__name__ for base in bases])
        print '  - \033[1;36m%s\033[37m fournit \033[1;36m%d\033[37m implementations of \033[1;32m%s\033[37m :' % (plugin.id, len(exts), interface)
        for klass in exts :
          if isinstance(klass, ServiceOffer):
            print '    -> \033[34mfactory of:\033[37m', klass.protocol,
          else :
            try :
              print '    -> \033[34mclass:\033[37m', klass.__name__.ljust(30),
            except AttributeError :
              print '    -> \033[34mclass:\033[37m', str(klass).ljust(30),
            try :
              print '\033[34mid:\033[37m', klass.identifier,
            except AttributeError :
              print 'NO IDENTIFIER',
          print
        print
    print

#  for plugin in application :
#    display_plugin_infos(plugin)

class PluginManager(EnvisageApplication):

  def __init__(self, plugins=None, **traits):
    EnvisageApplication.__init__(self, plugins, #@UndefinedVariable
                                 service_registry=ServiceRegistery(),
                                 **traits)
#    self.service_registry = ServiceRegistery()

  def _home_default(self):
    """ Trait initializer. """
    home = EnvisageApplication._home_default(self) #@UndefinedVariable
    return UserConfigDir(home.decode(ENCODING))


class Application(InheritableSingleton):
  u"""
  Unique object used to create and store services, handle global settings and
  paths, ...
  
  Services are the objects that do the actual work!
  Services provides cross-application features.
  These objects are based on  
  `Envisage Framework<http://github.enthought.com/envisage/envisage_core_documentation/index.html>`
  and mainly on EnvisageApplication class that is called "PluginManager" here.

  To create a new service, one can use "init<Service>" method that select
  for you the best provider for this service, builds it and stores it.
  Once created, service can be reached thanks to "get<Service>" methods.
  "init<Service>" creates service only at the first call as services are often
  complex classes.
  If you want to change provider later, use "set<Service>" method.
  
  If a service has not its corresponding set/get/init<Service> method, one can
  use setService, getService and initService.

  Builtin services are :
    - ProcessHandler : runs shell commands and allows user interactions (like subprocess module)
    - Logger : logging system (like logging module)
    - Recorder : records user sessions as Python script (using apptools)
    - DataConverter : converts, reads and writes data using pyLot.dataconverter framework

  .. warning ::
  
    Application is not fully implemented and may often change.
    Consider it as a work in progress feature.
  
  """

  _identifier = None
  _plugin_manager = None
  _process_handler = None
  _default_list = []

  FORCE_UI_GENERATION = False
  DEBUG_PLUGINS = False

  def initProcessHandler(self):
    if self._process_handler is None :
      self.setProcessHandler(DEFAULT_PROCESS_HANDLER)

  def setProcessHandler(self, process_handler):
    self._process_handler = process_handler

  def getProcessHandler(self):
    if self._process_handler is None :
#      print "Creating default process handler"
      self.initProcessHandler()
    return self._process_handler

  def setPluginManager(self, plugin_manager, default_list=None):
    if default_list is None :
      default_list = []
    self._default_list = default_list
    self._plugin_manager = plugin_manager
    confdir = self.home
    if not confdir.check() :
      raise ErrConfDirNotValid

  def initPluginManager(self, default_list=None):
    if default_list is None :
      default_list = []
    if self._plugin_manager is None :
      if self.identifier is None :
        identifier = 'pyLot'
      else :
        identifier = self.identifier
      pm = PluginManager(id=identifier)
      self.setPluginManager(pm, default_list=default_list)

  def getPluginManager(self):
    if self._plugin_manager is None :
#      print "Creating default plugin manager"
      self.initPluginManager()
    return self._plugin_manager

  def getIdentifier(self):
    if self._identifier is None :
      warnings.warn(u'APPLICATION.identifier not defined, applications using extensions may fail')
    return self._identifier

  def setIdentifier(self, identifier):
#    print 'IDENTIFIER=', identifier
    self._identifier = identifier
    # Create confdir if necessary

  def initIdentifier(self, identifier):
    if self._identifier is None :
      self.setIdentifier(identifier)

  home = userconf = property(fget=lambda self:self.getPluginManager().home)
  identifier = property(fget=getIdentifier, fset=setIdentifier)

  def setRecorder(self, recorder):
    self.recorder = recorder

  def getRecorder(self):
    try :
      self.recorder
    except AttributeError :
#      print "Creating default script recorder"
      self.recorder = Recorder()
    return self.recorder

  def getService(self, interface, target=None):
    if target is None :
      pm = self.getPluginManager()
      service = pm.get_service(interface)
      if service is None :
        raise ErrServiceNotFound(interface)
      else :
        return service
    else :
      return target

  def initRecorder(self):
    set_recorder(self.getRecorder())

  def startRecorder(self):
    self.recorder.clear()
    self.recorder.recording = True
    self.record(u'from pyLot.core import *')

  def stopRecorder(self):
    self.recorder.recording = False
    print '\n\nScript enregistré:\n', self.recorder.get_code()

  def record(self, pycode):
    self.recorder.record(pycode)

  def addPlugin(self, plugin):
    pm = self.getPluginManager()
    pm.add_plugin(plugin)

  def loadUserPlugins(self):
    plugins = find_app_plugins(confdir=self.home,
                               default_list=self._default_list)

    for plugin_class in plugins :
      plugin = plugin_class()
      for core_plugin_class in plugin.get_extensions('pyLot.plugins.coreplugins'):
        core_plugin = core_plugin_class()
        self.addPlugin(core_plugin)
      self.addPlugin(plugin)

  def init(self, identifier, plugin_list, core_plugin=CorePlugin):
    self.initIdentifier(identifier)
    self.initPluginManager(plugin_list)
    self.addPlugin(core_plugin())
    self.loadUserPlugins()
    self.start() # among other things, register service offers

  def start(self):
    pm = self.getPluginManager()
    pm.start()

  def stop(self):
    pm = self.getPluginManager()
    pm.stop()
    return 0

  def displayExtensionInformations(self):
    display_application_infos(self._plugin_manager)

  io = property(fget=lambda self:self.process.io)
  process = property(fget=getProcessHandler,
                     fset=setProcessHandler)

APPLICATION = Application()


if __name__ == '__main__' :

  plugin_dirs = [
    Path(u'/home/guillaume/prog/PyAero/applications'),
    Path(u'.').abspath()
    ]

  plugins = find_plugins(plugin_dirs=plugin_dirs)
  for plugin in plugins :
    print plugin()
